﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using Silvermoon.Shapes;
using Silvermoon.OpenGL;
using Silvermoon.Windows;
using Silvermoon.Core;
using Silvermoon.UI;
using Silvermoon.Images;

namespace Silvermoon.Controls
{
    /// <summary>
    /// Textbox control to input and edit text in one ore more rows.
    /// </summary>
    public class TextBox : TextboxBase
    {
        #region fields

        const int ACCEPT_RETURN = 32;
        const int AUTO_HEIGHT = 64;

        private int xOffset;
        protected HighlightStringShape stringShape;
        private Margin padding = new Margin(10);
        private Shape watermark;

        #endregion
        #region ctor/dtos

        public TextBox()
            : base()
        {
            stringShape = new HighlightStringShape
            {
                LineAlignment = Alignment.Near,
                Alignment = Alignment.Near,
                LineMode = LineMode.SingleLine,
                HighlightTextColor = PhoneColors.Accent
            };
            Shapes.AddLast(stringShape);
            Shapes.AddLast(CaretShape);
            TextColor = PhoneColors.Black;

        }

        protected override void Dispose(bool dispose)
        {
            if (watermark != null) Shapes.Remove(watermark);
            DisposeObject(watermark);

            base.Dispose(dispose);
        }

        #endregion
        #region accessors

        public static PropertyAccessor<TextBox, int> XOffsetProperty = PropertyAccessor.Register<TextBox, int>("XOffset",
            c => c.XOffset, (c, v) => c.XOffset = v);

        #endregion
        #region props

        /// <summary>
        /// Gets or sets the shape which is displayed over the textbox if the text is empty.
        /// If set, this should be a shape with an opacity.
        /// </summary>
        public Shape Watermark
        {
            get { return watermark; }
            set
            {
                if (watermark != value)
                {
                    Shape oldValue = watermark;
                    watermark = value;
                    OnWatermarkChanged(oldValue);
                }
            }
        }

        public Alignment Alignment
        {
            get { return stringShape.Alignment; }
            set
            {
                if (stringShape.Alignment != value)
                {
                    stringShape.Alignment = value;
                    Invalidate(InvalidationFlag.Appearance);
                }
            }
        }


        public Margin Padding
        {
            get { return padding ?? Margin.Zero; }
            set
            {
                if (!Padding.Equals(value))
                {
                    padding = value;
                    Invalidate(InvalidationFlag.Layout);
                }
            }
        }

        /// <summary>
        /// Gets or sets the number of pixels between each character.
        /// </summary>
        public int CharSpace
        {
            get { return stringShape.CharSpace; }
            set
            {
                if (stringShape.CharSpace != value)
                {
                    stringShape.CharSpace = value;
                    Invalidate(InvalidationFlag.LayoutAppearance);
                }
            }
        }

        ///// <summary>
        ///// Gets or sets the scale factor for the font.
        ///// </summary>
        //public float FontScale
        //{
        //    get { return stringShape.FontScale; }
        //    set
        //    {
        //        if (stringShape.FontScale != value)
        //        {
        //            stringShape.FontScale = value;
        //            Invalidate(InvalidationFlag.LayoutAppearance);
        //        }
        //    }
        //}

        public int MaxXOffset
        {
            get { return Math.Max(0, stringShape.TextSize.Width - ClientWidth); }
        }

        public int XOffset
        {
            get { return xOffset; }
            set
            {
                value = ValidateXOffset(value);
                if (xOffset != value)
                {
                    int oldValue = xOffset;
                    xOffset = value;
                    OnXOffsetChanged(oldValue);
                }
            }
        }

        /// <summary>
        /// Gets or sets a value that indicates how the text editing control responds when the user presses the ENTER key.
        /// If set to true, the TextBox is also enabled to show multiple lines.
        /// </summary>
        public override bool AcceptReturn
        {
            get { return (bitmask & ACCEPT_RETURN) != 0; }
            set
            {
                if (AcceptReturn != value)
                {
                    bitmask = (ushort)(value ? bitmask | ACCEPT_RETURN : bitmask & ~ACCEPT_RETURN);
                    OnAcceptReturnChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the color for highlighted text.
        /// </summary>
        public override Color HighlightColor
        {
            get { return stringShape.HighlightTextColor; }
            set
            {
                if (HighlightColor != value)
                {
                    stringShape.HighlightTextColor = value;
                    OnHighlightColorChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets whether thei height of the textbox depends on the number of lines inside.
        /// See also <seealso cref="P:MinLines"/> and <seealso cref="P:MaxLines"/>
        /// </summary>
        public bool AutoHeight
        {
            get
            {
                return (bitmask & AUTO_HEIGHT) != 0;
            }
            set
            {
                if (AutoHeight != value)
                {
                    bitmask = (ushort)(value ? bitmask | AUTO_HEIGHT : bitmask & ~AUTO_HEIGHT);
                    Invalidate(InvalidationFlag.LayoutAppearance);
                }
            }
        }

        /// <summary>
        /// Gets the total height for all lines.
        /// </summary>
        public int TotalHeight
        {
            get
            {
                return stringShape.TextSize.Height + padding.Height;
            }
        }

        /// <summary>
        /// Gets the rectangle that represents the client area of the control.
        /// </summary>
        /// <value></value>
        public override Rectangle ClientBounds
        {
            get
            {
                return padding.Layout(base.ClientBounds, Size);
            }
        }

        #endregion
        #region methods

        protected virtual void OnXOffsetChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.Appearance);
        }

        private void OnWatermarkChanged(Shape oldValue)
        {
            if (oldValue != null)
            {
                Shapes.Remove(oldValue);
                oldValue.Dispose();
            }
            Invalidate(InvalidationFlag.Appearance);
        }

        /// <summary>
        /// Gives the textbox the focus and shows the keyboard.
        /// </summary>
        public void SetFocus()
        {
            var w = Window;
            if (w == null) throw new ArgumentNullException("Window", "TextBox must be hosted in a Window or Page control.");
            Window.IsKeyboardVisible = true;
            BringIntoView(true);
            Focus();
        }

        protected virtual void OnHighlightColorChanged()
        {
            Invalidate(InvalidationFlag.Color);
        }

        /// <summary>
        /// Occurs after text is changed either by setting Text property, or by inserting or remove one ore more characters.
        /// See also <typeparamref name="OnTextAssigned"/>.
        /// </summary>
        protected override void OnTextChanged(string oldValue)
        {
            clip = Rectangle.Empty;
            stringShape.Text = GetDisplayText();
            XOffset = Math.Min(xOffset, MaxXOffset);
            base.OnTextChanged(oldValue);
        }

        /// <summary>
        /// Gets the text which is actually displayed.
        /// </summary>
        protected virtual string GetDisplayText()
        {
            return Text;
        }

        protected override void OnLayout()
        {
            if (CheckFlag(InvalidationFlag.Appearance)) UpdateAppearance();
            stringShape.Bounds = GetTextBounds();
            if (CheckFlag(InvalidationFlag.Size | InvalidationFlag.Layout | InvalidationFlag.Margin | InvalidationFlag.Appearance)) LayoutContent();

            stringShape.PreLoad();
            if (AutoHeight) Height = TotalHeight;

            base.OnLayout();
        }

        private void LayoutContent()
        {
            stringShape.Location = new Point(stringShape.X - xOffset, stringShape.Y);
            CaretShape.Visible = IsCaretVisible;
            CaretShape.Bounds = GetCaretRect();
            if (watermark != null) watermark.Bounds = ClientBounds;
        }

        private void UpdateAppearance()
        {
            if (watermark != null)
            {
                Shapes.Remove(watermark);
                if ((string.IsNullOrEmpty(Text))) Shapes.AddLast(watermark);
            }
        }

        /// <summary>
        /// Due to performance issues, the string shape must not always render the complete string, but only the visible part.
        /// </summary>
        private void SetRange()
        {
            Rectangle clip = GetClipRect();
            clip.X += xOffset;
            int index0 = Math.Max(0, stringShape.GetIndexFromPoint(clip.Location));
            int index1;
            if (AcceptReturn)
            {
                index1 = stringShape.GetIndexFromPoint(new Point(clip.Right - padding.Width, clip.Bottom)) + 1;
            }
            else
            {
                index1 = stringShape.GetIndexFromPoint(new Point(clip.Right - padding.Width - 1, clip.Top)) + 1;
            }
            stringShape.Range = new TextSpriteRange(index0, index1 - index0);

        }

        private Rectangle GetTextBounds()
        {
            Rectangle r = ClientBounds;
            if (AutoHeight) r.Height = int.MaxValue;
            return r;
        }

        private Rectangle GetBoundsFromCharIndex(int index)
        {
            bool eol = false;
            if (index >= Text.Length)
            {
                eol = true;
                index = Text.Length - 1;
            }
            if (index < 0) index = 0;
            Rectangle r = stringShape.GetCharRect(index);
            if (r.IsEmpty) r = new Rectangle(0, 0, 0, Font.CharHeight);
            if (eol)
            {
                r.Offset(r.Width, 0);
            }
            return r;
        }

        /// <summary>
        /// Gets the rectangle for the caret bounds.
        /// </summary>
        /// <returns>Rectangle.</returns>
        protected override Rectangle GetCaretRect()
        {
            Rectangle rect = Text.Length > 0 ? GetBoundsFromCharIndex(SelectionStart) : new Rectangle(0, 0, 0, Font.CharHeight);
            rect.Width = 3;
            if (AcceptReturn) rect = FixCharacterIsReturn(rect);
            rect.Offset(padding.LeftZero - xOffset, padding.TopZero);
            return rect;
        }

        /// <summary>
        /// Since a return is at the end of the line before line break, the caret rect must be set to the new line at the beginning.
        /// </summary>
        private Rectangle FixCharacterIsReturn(Rectangle rect)
        {
            int index = Math.Max(0, SelectionStart);
            if (index == Text.Length && index > 0 && Text[index - 1] == '\n')
            {
                rect.Y += Font.CharHeight;
                rect.X = 0;

            }
            return rect;
        }

        /// <summary>
        /// Gets the rectangle for the selection start bounds.
        /// </summary>
        /// <returns>Rectangle.</returns>
        protected override Rectangle GetSelectionStartRect()
        {
            Rectangle rect = GetBoundsFromCharIndex(SelectionStart);
            rect.Offset(padding.LeftZero - xOffset, padding.TopZero);
            return rect;
        }

        /// <summary>
        /// Gets the rectangle for the selection end bounds.
        /// </summary>
        /// <returns>Rectangle.</returns>
        protected override Rectangle GetSelectionEndRect()
        {
            Rectangle rect = GetBoundsFromCharIndex(SelectionStart + SelectionLength);
            rect.Width = 0;
            rect.Offset(padding.LeftZero - xOffset, padding.TopZero);
            return rect;

        }

        /// <summary>
        /// Ensures that the caret is visible in the none overlapped area of the window.
        /// </summary>
        /// <param name="animated"></param>
        protected override void EnsureCaretIsVisible(bool animated)
        {
            if (Parent != null)
            {
                Rectangle bounds = GetCaretRect();
                int x = bounds.X; // -padding.LeftZero;
                if (x < 0) SetXOffset(bounds.X + xOffset - padding.LeftZero, animated);
                else if (x > ClientWidth) SetXOffset(bounds.X - ClientWidth + xOffset - padding.LeftZero, animated);

                bounds.Offset(X, Y);
                if (!AcceptReturn)
                {
                    bounds.Y = this.Y;
                    bounds.Height = this.Height;
                }
                else
                {
                    bounds.Y -= padding.TopZero;
                    bounds.Height += padding.Height;
                }
                Parent.BringIntoView(bounds, animated);
            }
        }

        protected void SetXOffset(int value, bool animated)
        {
            if (animated)
            {
                IntTransition transition = new IntTransition { Duration = 250, To = value };
                XOffsetProperty.Animate(this, transition);
            }
            else
            {
                XOffsetProperty.StopAnimation(this);
                XOffset = value;
            }
        }

        /// <summary>
        /// Performs the needs to display the given selection.
        /// </summary>
        /// <param name="start">Start position in Text.</param>
        /// <param name="length">Length of selection.</param>
        protected override void SelectCore(int start, int length)
        {
            stringShape.ClearHighlights();
            if (length > 0)
            {
                stringShape.HighlightRange(start, start + length);
            }
            Invalidate(InvalidationFlag.Appearance);
        }

        protected internal override void OnBeginPan(PanEventArgs e)
        {
            base.OnBeginPan(e);
            if (!e.Handled)
            {
                if (e.HorizontalMotion)
                {
                    e.Handled = true;
                    e.Value = new Point(-xOffset, 0);
                }
            }
        }

        /// <summary>
        /// When overriden, scrolls the content of the textbox to an appropriate location.
        /// This method occurs at OnPan events.
        /// </summary>
        /// <param name="e"></param>
        protected override void ScrollText(PanEventArgs e)
        {
            if (!AcceptReturn)
            {
                XOffset = -e.Value.X;
            }
        }

        /// <summary>
        /// Gets the index for the character under the specified point.
        /// </summary>
        /// <param name="point">Point coordinates in pixels relative from the top/left corner of the control.</param>
        /// <returns>Index if the character unter that point.</returns>
        protected override int GetIndexFromPoint(Point point)
        {
            point.Offset(Math.Max(0, -padding.LeftZero + xOffset), Math.Max(0, padding.TopZero));
            point.Y = AcceptReturn ? Math.Min(stringShape.TextSize.Height, point.Y) : 0;
            return stringShape.GetIndexFromPoint(point);
        }

        /// <summary>
        /// Creates the default shape if none is specified.
        /// </summary>
        /// <returns></returns>
        protected override Shape CreateDefaultBackground()
        {
            //return PhoneColors.White;
            return new TextBoxShape();
        }

        protected override Shape CreateDefaultChrome()
        {
            return new BackgroundShape { Texture = SysImages.TextBoxChrome, Style = RenderStyle.Edged, Color = PhoneColors.Accent };
        }

        public void Highlight(int index)
        {
            stringShape.Highlight(index);
            Invalidate(InvalidationFlag.Appearance);
        }

        public void ClearHighlights()
        {
            stringShape.ClearHighlights();
            Invalidate(InvalidationFlag.Appearance);
        }

        protected override void UpKey()
        {
            Rectangle rect = GetCaretRect();
            rect.Y -= Font.CharHeight;
            int index = stringShape.GetIndexFromPoint(rect.Location);
            if (index >= 0)
            {
                Select(index, 0);
                EnsureCaretIsVisible(false);
            }
        }

        protected override void DownKey()
        {
            Rectangle rect = GetCaretRect();
            rect.Y += Font.CharHeight;
            int index = stringShape.GetIndexFromPoint(rect.Location);
            if (index >= 0)
            {
                Select(index, 0);
                EnsureCaretIsVisible(false);
            }
        }

        private Rectangle clip;

        protected internal override void OnRender(Renderer renderer)
        {
            UpdateClip(renderer);
            base.OnRender(renderer);
        }

        private void UpdateClip(Renderer renderer)
        {
            Rectangle clipRect = renderer.GetCurrentBounds();
            clipRect.X -= xOffset;
            if (clipRect != this.clip)
            {
                this.clip = clipRect;
                SetRange();
            }
        }

        protected override void OnFontChanged()
        {
            stringShape.Font = Font;
            base.OnFontChanged();
        }

        protected virtual void OnAcceptReturnChanged()
        {
            stringShape.LineMode = AcceptReturn ? LineMode.WordWrap : LineMode.SingleLine;
            XOffset = 0;
            Invalidate(InvalidationFlag.Appearance);
        }

        private int ValidateXOffset(int value)
        {
            if (value < 0) value = 0;
            else
            {
                int textSize = MaxXOffset;
                int max = Math.Max(0, MaxXOffset);
                if (value > max) value = max;
            }
            return value;
        }


        #endregion
    }
}
